【第1304期】聊一聊Redux的前身Flux
前言
抽奖这东西,欢喜就好,祝各位端午节安康。今日早读文章由网易考拉海购@Gloria投稿分享。
@Gloria,前端工程师,就职于网易考拉海购
正文从这开始~
现在诸多状态管理方案涌现,每种方案的背后都有支撑其实现的思想,而这些思想并不是“空穴来风”,都是为了解决开发中出现的各种问题而诞生。
接下来的会深入探讨时下比较流行的两种状态管理方案,Redux,Mobx。
为了深入了解Redux,不可避免地就要谈到它的前身Flux。
概念
在正文开始之前,我们需要理解在平时使用诸如react.js
、vue.js
这类MV*框架时接触到Model和View的概念。
一个完整的交互流程就如上图所示。
View
View,意为“视图”,即最终在浏览器上看到的页面元素。
Model
Model,翻译过来就是“模型”,那…什么是“模型”呢?且看下面这些代码。
<div>
<p>{{a.b}}</p>
<p>{{a.c}}</p>
<p>{{a.d}}</p>
</div>
上面这一段代码,其中a.b
,a.c
,a.d
,每当这些属性值发生改变之后,框架会帮助我们生成View。
如果我们再稍微宏观地看待这一问题,其实可以将a
这个对象看作是data(数据),而上面的html代码就是template(模板),于是就有了这种理解:框架通过将data
应用到template
上,最后生成View,即b
过程。
在这里data
+template
就是Model
,即所谓的“模型”,而通常意义上template
是固定不变的,不会动态发生变化(这种动态变化已经被涵盖在模板本身的语法中了),所以大多数时候我们实现的各种交互就是改变data
上属性的过程,示意图中的a
过程。
目前开发中存在的问题
ok,介绍完Model和View这两个概念后,在这两个抽象层面上谈一谈平时开发过程中遇到的问题。
碎片化修改
我们实现交互基础就是操作Model,就拿上面那个代码片段来说,操作Model就是修改a.b
,a.c
,a.d
,于是操作这个Model就会像下图所展示的情况一样,修改操作会“碎片化”地存在于整个组件文件的各个角落。
对于没有严格开发模式限制的工程,一旦页面复杂度上去了,如果多人维护这样的代码,添加feature的时候可以说会比较刺激了。
大多数情况的表情应该是这样的
数据流捉摸不定
1. 复杂的数据流
先来谈一谈vue.js
之类基于检测数据变动实现局部更新的MVVM框架,这些框架提供了多种多样影响Model的方式。
看一看这张图
最明显的,跟上面那张图相比,增加了从View到Model这一个方向,这种改变自然是框架“双向数据绑定”所带来的。毫无疑问,这种feature给我们带来了一定的便利,但与此同时,它会使得最终生成View的逻辑更加扑朔迷离,为什么这么说呢?
从另外一个角度看待这个问题,最终到View的不同路径数越多,就代表生成View的方式越多,生成View的方式越多,代码的可预测性就越弱。
很显然,在这张图当中,以View做为终点的路径还是不少的,以碎片化修改为起点的路径有2条,以View作为起点的路径有3条。
从路径数量这个角度,很直观地就可以得出这类框架设计对于代码可维护性是不友好的。
2. 简单的数据流
但是,也有一些框架数据流是比较简单的(比如React),改变Model的方式仅限于手动调用setState,或者View触发setState,在代码的predicatable(可预测性)方面有比较大的优势。
OK,以上这些与这次的主题有什么关系呢?
Flux
上面已经谈到了现在MV*框架中存在的问题,比如vue,react等都仅仅是视图层框架,也就是说,它们只负责渲染View,而对于Model的变化没有统一的管理方案。
Flux的出现其实就是为了管理Model的变化,使得应用的可伸缩性,和代码的可预测性更强。
单向数据流基础
Flux其实就是在React单向数据流的基础上做了一层对Model的管理,那就看一看它是如何借鉴的。
相比其它框架设计,最大的不同之处就是:React没有View-->Model
这个方向。就拿上面复杂数据流方案来说,以View为起点的数据流路径就可以减少两条,保证了最终生成View的逻辑是相对清晰的。
如何看待Flux架构
Flux其实提供了一整套Model修改模式。这种模式的初衷,在我看来,就是为了提高代码的可预测性,再通俗一点就是,当你看到了一段代码时,让你更清晰地知道它会做什么。
为什么这么说呢?我们在维护工程时无外乎就是扮演两个角色:使用者和定义者。而往往我们在代码中确很少体现这两种角色抽象,最多也只是在文档和代码规范层面,任你玩出花来,也很难做到比较高的通用性。
再具体一点,Flux将使用者和定义者的抽象引入了Model的修改过程。类似Clent-Service架构,如果使用者(客户端)想要修改数据库,必须通过调用定义者(服务端)提供的接口实现。
1. 请求
在Flux中,request(请求)等价于action,触发一个action相当调用一次接口,action的type字段相当于接口地址,其它字段相当于payLoad(请求参数)。
action应该是一个对象:
{
type: 'delete-todo', //接口地址
todoID: '1234' //payLoad
};
既然将action当作了request,那么我们应该如何实现server(服务器)呢?
2. 路由
就像Clent-Service中一样,server接收请求并将不同的请求映射为相应的数据库修改操作。将server中接收请求的部分称为router(路由)。
一个router应该长这样:
let router = (function router(){
let dataBase = {todos: []}; //模拟数据库的对象
return function(request){
switch(request.type){
case 'ADD_TODO': deleteToDo(request, dataBase); break;
...
}
};
})();
发送一个请求:
router({type: 'delete-todo', todoID: '1234'});
deleteToDo()
其实就是相应修改数据库的操作,里面的具体逻辑需要我们自己写,显然,删除一个”待办事项”,deleteToDo()
应该长下面这样:
function deleteToDo(request, dataBase){
let todos = dataBase.todos;
for(let i = 0; i < todos; i++){
if(todos[i].id === request.todoID){
todos.splice(i, 1);
return;
}
}
}
ok,到目前为止,整个流程已经跑通了。定义一个request,使用router发送这个request,router根据request地址分配相应的数据库处理逻辑,于是就得到了下面这种抽象:
用上面这种架构已经可以勉强驾驭一些比较简单的应用场景,而面对稍微复杂一点的应用场景就捉襟见肘了,为什么这么说呢?
这种架构最基本的应用单元就是组件,每个组件的Model其实就是对应的dataBase,如果我们想在某个组件内修改其它组件的dataBase,就需要拿到这个组件的router,而”拿router”这件事可并没有那么简单。。大体上根据组件之间的关系,分为3种情况:父子关系、爷孙关系和兄弟关系,于是就会出现下面这种情况。
为了解决这一问题,Flux的另一个概念就来了,dispatcher。
3. 请求分发器
Flux的dispatcher(请求分发器),其实解决了上述问题。
dispatcher相对各个组件而言是全局性的,它可以将请求发送到所有的router,用户无需知道他需要请求的router,让每个router自行处理进来的request,这种抽象其实是将request视为全局性请求,一个request可以同时操作多个dataBase。
当然,dispatcher不会自己寻找它需要分发到的router,我们需要调用register()
方法手动注册router
dispatcher.register(router);
在注册好router后,直接调用dispatcher的dispatch()
方法即可,可以像下面这样发送一个request:
dispatcher.dispatch({type: 'delete-todo', todoID: '1234'});
默认情况下,Flux会按照注册顺序依次将request放进router。如果我们希望自定义发送request后,部分router的执行顺序怎么办?Flux提供了waitFor()
方法。
举个例子:routerA接收到请求之后,希望依次经过routerB,和routerC,可以像下面伪代码这样实现:
let tokenB = dispatcher.register(routerB);
let tokenC = dispatcher.register(routerC);
let routerA = function(request){
switch(request.type){
case 'ADD_TODO': dispatcher.waitFor(tokenB, tokenC); break;
...
}
};
OK,你必须提前拿到routerB和routerC的token
,然后按照顺序传入waitFor()
方法(个人认为这种”拿token”,无异于上面提到的”拿router”,是一个设计缺陷)。
4. 数据库
dataBase(数据库)其实就代表了组件的state(状态)。
而Flux将router和dataBase视为一体,将请求的解析和数据库的修改统一交给store来处理。
store.reduce()
相当于router,而store._state
则相当于dataBase,于是就有了下面这种架构
最后,Flux采用了向外抛事件的方式,将_state
映射到Model的工作交给用户去解决。
你可以调用store.addListener()
方法,传入回调函数即可监听到_state
的变化。
store.addListener(() => {
let state = store.getState();
...映射到Model的操作...
});
结语
Flux的一整套抽象(action,dispatcher,store),在单向数据流的基础上可以提高应用的可维护性和代码的可预测性。然而,全局action+多store的架构面对复杂的应用依然不能很好地解决复杂数据流的问题,waitFor()
虽然可以满足自定义多store接收action的顺序,但是它会让数据流变得复杂,难以维护。
Redux作为Flux的继承者,单store的架构其实就很好地避免了上述问题,之后的文章会深入分析Redux是如何在Flux的基础上改进自身架构的。
参考:
Flux官方介绍:In Depth Overview
Flux官方仓库:github.com/facebook/flux
关于本文
作者:@Gloria
原文:
https://zhuanlan.zhihu.com/p/38050036
最后,为你推荐